Java基础(十三)——类加载机制和反射

java.lang.reflect包下的接口和类,包括Class(类)、Method(方法)、Field(成员变量)、Constructor(构造器)、Array(数组)等。Type接口:是Class所实现的接口;ParameterizedType接口:代表一个带泛型参数的类型。

类的加载、连接和初始化

当java命令运行某个java程序时,会启动一个java虚拟机进程。同一个JVM的所有线程、所有变量都处于同一个进程里,它们都是用该JVM进程的内存区。
当程序主动使用某个类时,如果该类还未被加载到内存中,JVM将进行类的加载或类初始化:加载、连接、初始化。

类的加载

类的加载是将类的class文件读入内存,并为之创建一个java.lang.Class对象(类是某一类对象的抽象,即类也是一种对象)。
类的加载由类加载器完成,类加载器由JVM提供,JVM提供的类加载器通常称为系统加载器。可通过继承ClassLoader来创建自己的类加载器。
通过使用类加载器,可以从不同来源加载类的二进制数据:
(1) 从本地文件系统加载class文件,即前面绝大部分示例。
(2) 从JAR包加载class文件。如JDBC编程时用到的数据库驱动类就放在JAR文件中。
(3) 通过网络加载class文件。
(4) 把一个java源文件动态编译,并执行加载。

类的连接(包括验证、准备、解析)

当类被加载后,系统就为之生成一个对应的Class对象,接着将会进入连接阶段,把类的二进制数据合并到JRE。类的连接可分为如下三个阶段:
(1)验证:检验被加载的类是否有正确的内部结构,并和其它类协调一致。
(2)准备:为类变量分配内存,并设置默认初始值。
(3)解析:将类的二进制数据中的符号引用替换成直接引用。

类的初始化

虚拟机对类进行初始化,主要是对类变量(静态变量)进行初始化
1) 在Java中对类变量指定初始值的两种方式:
(1) 声明类变量时指定初始值;
(2) 使用静态初始化块为类变量指定初始值。
2) JVM初始化一个类的步骤
(1) 假如这个类还没有被加载和连接,则程序先加载并连接该类;
(2) 假如该类的直接父类还没有被初始化,则先初始化其直接父类(一直递归执行其父类);
(3) 假如该类中有初始化语句,则系统依次执行这些初始化语句。

类初始化的时机

Java首次通过以下6种方式来使用某个类或接口时,系统就会初始化该类或接口。(什么情况,Java类会被加载到JVM)
1) 创建类的实例。为某个类创建实例的方式包括:new关键字、反射、反序列化;
2) 调用某个类的静态方法(类方法);
3) 访问某个类或接口的静态方法(类方法),或为该静态变量赋值;
4) 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。如使用Class类的静态方法forName()会强制初始化该类,Class.forName(“Person”),如果系统还未初始化Person类,则初始化Person类,并返回Person类对应的java.lang.Class对象;
5) 初始化某个类的子类。当初始化某个类的子类时,该子类的所有父类都会被初始化;
6) 直接使用java.exe命令来运行某个主类。当运行某个主类时,程序会先初始化该主类。
注意:
1) 对于final static类型变量(final静态变量),如果该静态变量的值在编译时就可以确定下来,那么这个静态变量相当于常量,Java编译器会在编译时直接把这个静态变量出现的地方替换成它的值。因此程序即使使用该静态变量,也不会导致该类的初始化。反之,如果在编译时final静态变量的值不能被确定下来,则必须在运行时才能确定下来。
2) 当使用ClassLoader类的loadClass()方法来加载某个类时,该方法只是加载该类,并不会执行该类的初始化。使用Class的forName()静态方法才会导致强制初始化该类。

类加载器

类加载器是将类的.class文件读入内存,并为之创建一个java.lang.Class对象。

类加载器

在Java中,一个类用其全限定类名(包括包名和类名);但在JVM中,一个类用其全限定类名和其类加载器作为唯一标识。
Java启动时,会形成由三个类加载器组成的初始类加载器结构。
(1) Bootstrap ClassLoader(根类加载器):负责加载Java的核心类。它是由JVM自身实现的,不是java.lang.ClassLoader的子类
获取根类加载器所加载的核心类库:

1
2
3
4
5
6
7
8
9
public class BootstrapTest{
public static void main(String[] args){
//获取根加载器加载的全部url数组
URL[] urls = sun.misc.path.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++){
System.out.println(urls[i].toExternalForm());
}
}
}

(2)Extension ClassLoader(扩展类加载器):加载JRE的扩展目录(%JAVA_HOME%\jre\lib\ext或由java.ext.dirs系统属性指定的目录)中JAR包的类。可以把自己开发的类打包成JAR文件,放入JAVA_HOME\jre\lib\ext。(我的目录是D:\Software\Java\jre\lib\ext)
(3)System ClassLoader(系统(或应用)类加载器):在JVM启动时加载来自java命令的-classpath选项、java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。可通过ClassLoader类的静态方法getSystemClassLoader()来获取系统类加载器。

Java中4种类加载器的层次结构
缓存机制将会保证所有加载过的Class都户被缓存,当程序需要使用某个Class类时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。
获取系统类加载器的加载路径,通常是由CLASSPATH环境变量指定,如果操作系统没有指定CLASSPATH环境变量,则默认以当前路径作为系统类加载器的加载路径。

类加载机制

1) 全盘负责。当一个类加载器负责加载某个类时,该类所依赖和所引用的其它类也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。
2) 父类委托。先让父类加载器先加载该Class,只有在父类加载器无法加载该类时,才尝试从自己的类路径中加载该类。
3) 缓存机制。缓存机制将会保证所有加载过的Class都会被缓存,当程序需要使用某个Class类时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class之后,必须重新启动JVM,程序所做的修改才会生效的原因。
获取系统类加载器的加载路径,通常是由CLASSPATH环境变量指定,如果操作系统没有指定CLASSPATH环境变量,则默认以当前路径作为系统类加载器的加载路径。

自定义类加载器

JVM中除了根类加载器之外的所有类加载器都是ClassLoader子类的实例(根类加载器并不是Java实现的,而且程序通常无需访问根类加载器,因此访问扩展类加载器的父类加载器时返回null),自定义的类加载器可以通过继承ClassLoader类,并重写该ClassLoader所包含的所有方法。

通过反射查看类信息

Java程序中的许多对象在运行时都会出现两种类型:编译时类型和运行时类型。如Person p = new Student(); 生成一个p变量,该变量的编译时类型为Person,运行时类型为Student。

为了使程序在运行时发现对象和类的真实信息,有两个办法:

1) 假设编译时和运行时都完全知道类型的具体信息,使用instanceof运算符进行判断,再利用类型强制转换将其转换成其运行时类型的变量。
2) 编译时根本无法预知该对象和类可能属于哪些类,只能依靠运行时信息来获取真实信息,需要使用反射。

获取Class对象

1) 使用Class类的forName(String className)方法,其中传入的字符串参数为某个类的全限定包名(有完整包名);
2) 调用某个类的class属性。如Person.class将会返回Person类对应的Class对象。(这种方法更好,直接调用该类的Class对象那个,编译阶段就可检查,不用调用方法性能更好)
3) 调用某个对象的getClass()方法。它是Object类中的一个方法。

从Class中获取信息

从Class类提供的大量实例方法来获取Class对象所对应类的详细信息。如getConstructor()、getConstructors()、getMethod()、getMethods()、getField()、getFields()、getAnnotation()、getInterface()、getSuperclass()、int getModifiers()、getName()、getPackage()等。
可以通过反射获取整个类的结构,包括属性(field)、方法(method)、构造方法(constructor)等。三种反射场景:
1)通过Class对象来获取完整包名,如a.getClass().getName();
2)通过完整包名来实例化Class对象,如Class<?> c = Class.forName(“com.Licht._18.Student”); //需要用try…catch来处理异常
3)通过完整包路径类型来实例化Class对象,通过反射获取Class对象的构造器,通过构造器实例化对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Class<?> c = null;
try{
c = Class.forName(“com.Licht._18.Student”);
System.out.println(c.getName());
}catch(ClassNotFountException e){
e.printStackTrace();
}
Student s = null;
Constructor<?>[] cons = c.getConstructors(); //返回一个包含某些 Constructor 对象的数
// 组,反映此 Class 对象所表示的类的所有公共构造方法。
try{
s = (Student) cons[0].newInstance(“Licht”, 24); //第0个构造器
}catch(InstantiationException e){
e.printStackTrace();
}
1
2
3
4
5
6
7
8
9
10
11
12
//通过getConstructors()方法获取所有构造器
Class<?> c = null;
try{
c.forName(“com.Licht._18.Student”);
System.out.println(c.getName());
}catch(ClassNotFoundException e){
e.printStackTrace();
}
Constructor<?>[] cons = c.getConstructors();
for (Constructor<?> con : cons){
System.out.println(“构造器:” + con);
}
1
2
3
4
5
6
7
8
9
10
11
12
//通过getMethods()方法获取所有方法
Class<?> c = null;
try{
c.forName(“com.Licht._18.Student”);
System.out.println(c.getName());
}catch(ClassNotFoundException e){
e.printStackTrace();
}
Method[] mds = c.getMethod();
for (Method m : mds){
System.out.println(“方法:” + m);
}
1
2
3
4
5
6
7
8
9
10
11
12
//通过getDeclaredFields()方法获取所有属性
Class<?> c = null;
try{
c.forName(“com.Licht._18.Student”);
System.out.println(c.getName());
}catch(ClassNotFoundException e){
e.printStackTrace();
}
Field[] fs = c.getDeclaredFields();
for (Field f : fs){
System.out.println(“属性:” + f);
}

使用反射并操作对象

创建对象

调用方法

访问成员变量值

操作数组

使用反射生成动态代理

反射和泛型